package com.baidu.iit.pxp.converter;

import com.baidu.iit.pxp.constants.BpmnXMLConstants;
import com.baidu.iit.pxp.event.BoundaryEvent;
import com.baidu.iit.pxp.exception.XMLException;
import com.baidu.iit.pxp.io.InputStreamProvider;
import com.baidu.iit.pxp.model.*;
import com.baidu.iit.pxp.model.Process;
import com.baidu.iit.pxp.parser.*;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.xml.sax.SAXException;

import javax.xml.XMLConstants;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import javax.xml.transform.stax.StAXSource;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.validation.Validator;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.util.*;

/**
 * User: huangweili
 * Date: 14-4-24
 * Time: 下午6:44
 */
public class BpmnXMLConverter {

    protected static final Logger LOGGER = LoggerFactory.getLogger(BpmnXMLConverter.class);
    protected static final String BPMN_XSD = "com/baidu/iit/pxp/process.xsd";
    protected static final String DEFAULT_ENCODING = "UTF-8";

    protected static Map<String, BaseBpmnXMLConverter> convertersToBpmnMap = new HashMap<String, BaseBpmnXMLConverter>();

    protected ClassLoader classloader;
    protected DefinitionsParser definitionsParser = new DefinitionsParser();

    protected ImportParser importParser = new ImportParser();
    protected InterfaceParser interfaceParser = new InterfaceParser();
    protected ItemDefinitionParser itemDefinitionParser = new ItemDefinitionParser();

    protected ProcessParser processParser = new ProcessParser();
    protected SubProcessParser subProcessParser = new SubProcessParser();

    static {

        addConverter(new EndEventXMLConverter());
        addConverter(new StartEventXMLConverter());
        addConverter(new SequenceFlowXMLConverter());

        addConverter(new EventGatewayXMLConverter());
        addConverter(new ExclusiveGatewayXMLConverter());
        addConverter(new InclusiveGatewayXMLConverter());
        addConverter(new ParallelGatewayXMLConverter());
        addConverter(new TaskXMLConverter());
        addConverter(new ServiceTaskXMLConverter());


        /*
        // tasks
        addConverter(new BusinessRuleTaskXMLConverter());
        addConverter(new ManualTaskXMLConverter());
        addConverter(new ReceiveTaskXMLConverter());
        addConverter(new ScriptTaskXMLConverter());
        addConverter(new SendTaskXMLConverter());
        addConverter(new UserTaskXMLConverter());
        addConverter(new TaskXMLConverter());
        addConverter(new CallActivityXMLConverter());



        // data objects
        addConverter(new ValuedDataObjectXMLConverter(), StringDataObject.class);
        addConverter(new ValuedDataObjectXMLConverter(), BooleanDataObject.class);
        addConverter(new ValuedDataObjectXMLConverter(), IntegerDataObject.class);
        addConverter(new ValuedDataObjectXMLConverter(), LongDataObject.class);
        addConverter(new ValuedDataObjectXMLConverter(), DoubleDataObject.class);
        addConverter(new ValuedDataObjectXMLConverter(), DateDataObject.class);

        // catch, throw and boundary event
        addConverter(new CatchEventXMLConverter());
        addConverter(new ThrowEventXMLConverter());
        addConverter(new BoundaryEventXMLConverter());

        // artifacts
        addConverter(new TextAnnotationXMLConverter());
        addConverter(new AssociationXMLConverter());
        */
    }

    public static void addConverter(BaseBpmnXMLConverter converter) {
        addConverter(converter, converter.getBpmnElementType());
    }

    public static void addConverter(BaseBpmnXMLConverter converter, Class<? extends BaseElement> elementType) {
        convertersToBpmnMap.put(converter.getXMLElementName(), converter);
    }

    public void setClassloader(ClassLoader classloader) {
        this.classloader = classloader;
    }


    public void validateModel(InputStreamProvider inputStreamProvider) throws Exception {
        Schema schema = createSchema();

        Validator validator = schema.newValidator();
        validator.validate(new StreamSource(inputStreamProvider.getInputStream()));
    }

    public void validateModel(XMLStreamReader xmlStreamReader) throws Exception {
        Schema schema = createSchema();

        Validator validator = schema.newValidator();
        validator.validate(new StAXSource(xmlStreamReader));
    }

    protected Schema createSchema() throws SAXException {
        SchemaFactory factory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI);
        Schema schema = null;
        if (classloader != null) {
            schema = factory.newSchema(classloader.getResource(BPMN_XSD));
        }

        if (schema == null) {
            schema = factory.newSchema(BpmnXMLConverter.class.getClassLoader().getResource(BPMN_XSD));
        }

        if (schema == null) {
            throw new XMLException("BPMN XSD could not be found");
        }
        return schema;
    }

    public BpmnModel convertToBpmnModel(InputStreamProvider inputStreamProvider, boolean validateSchema, boolean enableSafeBpmnXml) {
        return convertToBpmnModel(inputStreamProvider, validateSchema, enableSafeBpmnXml, DEFAULT_ENCODING);
    }


    public BpmnModel convertToBpmnModel(InputStreamProvider inputStreamProvider, boolean validateSchema, boolean enableSafeBpmnXml, String encoding) {

        XMLInputFactory xif = XMLInputFactory.newInstance();
        if (xif.isPropertySupported(XMLInputFactory.IS_REPLACING_ENTITY_REFERENCES)) {
            xif.setProperty(XMLInputFactory.IS_REPLACING_ENTITY_REFERENCES, false);
        }

        if (xif.isPropertySupported(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES)) {
            xif.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);
        }

        if (xif.isPropertySupported(XMLInputFactory.SUPPORT_DTD)) {
            xif.setProperty(XMLInputFactory.SUPPORT_DTD, false);
        }

        InputStreamReader in = null;
        try {
            in = new InputStreamReader(inputStreamProvider.getInputStream(), encoding);
            XMLStreamReader xtr = xif.createXMLStreamReader(in);

            try {
                if (validateSchema) {

                    if (!enableSafeBpmnXml) {
                        validateModel(inputStreamProvider);
                    } else {
                        validateModel(xtr);
                    }


                    in = new InputStreamReader(inputStreamProvider.getInputStream(), encoding);
                    xtr = xif.createXMLStreamReader(in);
                }

            } catch (Exception e) {
                throw new RuntimeException("Could not validate XML with BPMN 2.0 XSD", e);
            }


            return convertToBpmnModel(xtr);


        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException("The bpmn 2.0 xml is not UTF8 encoded", e);
        } catch (XMLStreamException e) {
            throw new RuntimeException("Error while reading the BPMN 2.0 XML", e);
        } finally {
            if (in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                    LOGGER.info("Problem closing BPMN input stream", e);
                }
            }
        }
    }


    public BpmnModel convertToBpmnModel(XMLStreamReader xtr) {
        BpmnModel model = new BpmnModel();
        try {
            Process activeProcess = null;
            List<SubProcess> activeSubProcessList = new ArrayList<SubProcess>();
            while (xtr.hasNext()) {
                try {
                    xtr.next();
                } catch (Exception e) {
                    LOGGER.error("Error reading XML document", e);
                    throw new XMLException("Error reading XML", e);
                }

                if (xtr.isEndElement() && BpmnXMLConstants.ELEMENT_SUBPROCESS.equals(xtr.getLocalName())) {
                    activeSubProcessList.remove(activeSubProcessList.size() - 1);
                }


                if (xtr.isEndElement() && BpmnXMLConstants.ELEMENT_TRANSACTION.equals(xtr.getLocalName())) {
                    activeSubProcessList.remove(activeSubProcessList.size() - 1);
                }


                if (xtr.isStartElement() == false)
                    continue;

                if (BpmnXMLConstants.ELEMENT_DEFINITIONS.equals(xtr.getLocalName())) {
                    definitionsParser.parse(xtr, model);

                } else if (BpmnXMLConstants.ELEMENT_ERROR.equals(xtr.getLocalName())) {

                    if (StringUtils.isNotEmpty(xtr.getAttributeValue(null, BpmnXMLConstants.ATTRIBUTE_ID))) {
                        model.addError(xtr.getAttributeValue(null, BpmnXMLConstants.ATTRIBUTE_ID),
                                xtr.getAttributeValue(null, BpmnXMLConstants.ATTRIBUTE_ERROR_CODE));
                    }

                } else if (BpmnXMLConstants.ELEMENT_IMPORT.equals(xtr.getLocalName())) {
                    importParser.parse(xtr, model);

                } else if (BpmnXMLConstants.ELEMENT_ITEM_DEFINITION.equals(xtr.getLocalName())) {
                    itemDefinitionParser.parse(xtr, model);

                } else if (BpmnXMLConstants.ELEMENT_INTERFACE.equals(xtr.getLocalName())) {
                    interfaceParser.parse(xtr, model);

                } else if (BpmnXMLConstants.ELEMENT_PROCESS.equals(xtr.getLocalName())) {

                    Process process = processParser.parse(xtr, model);
                    if (process != null) {
                        activeProcess = process;
                    }

                } else if (BpmnXMLConstants.ELEMENT_DOCUMENTATION.equals(xtr.getLocalName())) {

                    BaseElement parentElement = null;
                    if (activeSubProcessList.size() > 0) {
                        parentElement = activeSubProcessList.get(activeSubProcessList.size() - 1);
                    } else if (activeProcess != null) {
                        parentElement = activeProcess;
                    }
                    new DocumentationParser().parseChildElement(xtr, parentElement, model);

                } else if (BpmnXMLConstants.ELEMENT_EXTENSIONS.equals(xtr.getLocalName())) {
                    new ExtensionElementsParser().parse(xtr, activeSubProcessList, activeProcess, model);

                } else if (BpmnXMLConstants.ELEMENT_SUBPROCESS.equals(xtr.getLocalName())) {
                    subProcessParser.parse(xtr, activeSubProcessList, activeProcess);

                } else if (BpmnXMLConstants.ELEMENT_TRANSACTION.equals(xtr.getLocalName())) {
                    subProcessParser.parse(xtr, activeSubProcessList, activeProcess);

                } else {

                    if (activeSubProcessList.size() > 0 && BpmnXMLConstants.ELEMENT_MULTIINSTANCE.equalsIgnoreCase(xtr.getLocalName())) {

                        new MultiInstanceParser().parseChildElement(xtr, activeSubProcessList.get(activeSubProcessList.size() - 1), model);

                    } else if (convertersToBpmnMap.containsKey(xtr.getLocalName())) {
                        if (activeProcess != null) {
                            BaseBpmnXMLConverter converter = convertersToBpmnMap.get(xtr.getLocalName());
                            converter.convertToBpmnModel(xtr, model, activeProcess, activeSubProcessList);
                        }
                    }
                }
            }

            for (Process process : model.getProcesses()) {
                processFlowElements(process.getFlowElements(), process);
            }

        } catch (Exception e) {
            LOGGER.error("Error processing BPMN document", e);
            throw new XMLException("Error processing BPMN document", e);
        }
        return model;
    }

    private void processFlowElements(Collection<FlowElement> flowElementList, BaseElement parentScope) {
        for (FlowElement flowElement : flowElementList) {
            if (flowElement instanceof SequenceFlow) {
                SequenceFlow sequenceFlow = (SequenceFlow) flowElement;
                FlowNode sourceNode = getFlowNodeFromScope(sequenceFlow.getSourceRef(), parentScope);
                if (sourceNode != null) {
                    sourceNode.getOutgoingFlows().add(sequenceFlow);
                }
                FlowNode targetNode = getFlowNodeFromScope(sequenceFlow.getTargetRef(), parentScope);
                if (targetNode != null) {
                    targetNode.getIncomingFlows().add(sequenceFlow);
                }
            } else if (flowElement instanceof BoundaryEvent) {
                BoundaryEvent boundaryEvent = (BoundaryEvent) flowElement;
                FlowElement attachedToElement = getFlowNodeFromScope(boundaryEvent.getAttachedToRefId(), parentScope);
                if (attachedToElement != null) {
                    boundaryEvent.setAttachedToRef((Activity) attachedToElement);
                    ((Activity) attachedToElement).getBoundaryEvents().add(boundaryEvent);
                }
            } else if (flowElement instanceof SubProcess) {
                SubProcess subProcess = (SubProcess) flowElement;
                processFlowElements(subProcess.getFlowElements(), subProcess);
            }
        }
    }

    private FlowNode getFlowNodeFromScope(String elementId, BaseElement scope) {
        FlowNode flowNode = null;
        if (StringUtils.isNotEmpty(elementId)) {
            if (scope instanceof Process) {
                flowNode = (FlowNode) ((Process) scope).getFlowElement(elementId);
            } else if (scope instanceof SubProcess) {
                flowNode = (FlowNode) ((SubProcess) scope).getFlowElement(elementId);
            }
        }
        return flowNode;
    }


}
